/// <reference path="Libraries/jasmine/jasmine.d.ts" />
/// <reference path="Core.ts" />

/**
 * Module for Jasmine specific helpers.
 */
module ExtSpec.Jasmine {
    'use strict';

    interface ISpyConfig {
        name: string;
        value?: any;
        action?: string;
    }

    /**
     * Discerns the action to execute on a spy from the given value.
     * @param {Object} value The value to analyze.
     * @returns {String}
     */
    function selectSpyAction(value: any): string {
        switch (ExtSpec.typeOf(value)) {
            case 'function':
                return 'fake';
            case 'array':
                return 'spy';
            case 'undefined':
                return 'none';
        }

        return 'return';
    }

    /**
     * Configures a spy using the provided details.
     * @param {jasmine.Spy} spy The spy to configure.
     * @param {Object} config The configuration for the spy.
     */
    function configureSpy(target: any, name: string, config: ISpyConfig): void {
        var value: any = config.value,
            action: string = config.action || selectSpyAction(value),
            spy: jasmine.Spy = target[name],
            newTarget: any;

        switch (action) {
            case 'return':
                spy.and.returnValue(value);
                break;
            case 'spy':
                newTarget = {};
                processSpies(newTarget, value);
                spy.and.returnValue(newTarget);
                break;
            case 'fake':
                spy.and.callFake(value);
                break;
            case 'call':
                spy.and.callThrough();
                break;
            case 'fluent':
                spy.and.returnValue(target);
                break;
        }
    }

    /**
     * Processes a single spy.
     * @param {Object} target The target on which to attach the spy.
     * @param {String} name The spy name.
     */
    function processSpy(target: any, name: string): void;
    /**
     * Processes a single spy.
     * @param {Object} target The target on which to attach the spy.
     * @param {Object} config The spy configuration object.
     */
    function processSpy(target: any, config: ISpyConfig): void;
    /**
     * Processes a single spy.
     * @param {Object} target The target on which to attach the spy.
     * @param {Object|String} config The spy name as a string or configuration object.
     */
    function processSpy(target: any, arg: any): void {
        var name: string,
            hasConfig: boolean,
            config: ISpyConfig;

        if (ExtSpec.isTypeOf(arg, 'st')) {
            name = arg.toString();
        } else if (ExtSpec.isObjectType(arg)) {
            config = arg;
            name = config.name;
            hasConfig = true;

            switch (config.action) {
                case 'object':
                    target[name] = {};
                    processSpies(target[name], config.value); //ignore jslint
                    return;
                case 'property':
                    target[name] = config.value;
                    return;
            }
        } else {
            return;
        }

        if (!ExtSpec.isTypeOf(target[name], 'fn')) {
            // the target is not a function, so assign it to a new spy
            target[name] = jasmine.createSpy(name);
        } else if (!jasmine.isSpy(target[name])) {
            // the target is a function but not a spy, so spy on it
            spyOn(target, name);
        }

        if (hasConfig) {
            configureSpy(target, name, config);
        }
    }

    /**
     * Processes an array of spy configurations and attaches them to a target object.
     * @param {Object} target The target on which to attach the spies.
     * @param {Object[]} spies The list of spies to attach to the target as strings, configuration objects or a combination of both.
     */
    function processSpies(target: any, spies: any[]): void {
        if (!ExtSpec.isObjectType(target)) {
            throw new Error('The target parameter must be of an object type.');
        }

        var i: number,
            len: number;

        if (ExtSpec.isTypeOf(spies, 'ar')) {
            for (i = 0, len = spies.length; i < len; i += 1) {
                processSpy(target, spies[i]);
            }
        }
    }

    /**
     * Concatenates the parts of an accessor name.
     * @param {String} current The current appended parts.
     * @param {String} next The next part to append.
     * @returns {String}
     */
    function accessorReducer(current: string, next: string): string {
        return current + next.slice(0, 1).toUpperCase() + next.slice(1);
    }

    /**
     * Creates and returns a jasmine spy on the given instance with the provided name and suffix.
     * @param {Object} instance The instance on which to create the spy.
     * @param {String} name The name to build into a spy.
     * @param {String} [suffix] The suffix to append to the spy.
     * @param {String} [prefix] The prefix to append to the spy name. Defaults to "get".
     * @returns {Function}
     */
    function createSpy(instance: any, name: string, suffix?: string, prefix?: string): jasmine.Spy {
        suffix = suffix || '';
        prefix = prefix || 'get';

        var separator = name.indexOf('@'),
            accessorName = separator > 0 ? name.slice(0, separator) : name,
            accessor: jasmine.Spy;

        accessorName = accessorName.split('.').reduce(accessorReducer, prefix) + suffix;

        if (accessorName in instance) {
            throw new Error('An accessor spy for "' + name + '" cannot be created because "' + accessorName + '" already exists.');
        }

        accessor = instance[accessorName] = jasmine.createSpy(accessorName);

        return accessor;
    }

    /**
     * Creates and returns a jasmine spy on the given instances with the provided name and type.
     * @param {Object} instance The instance on which to create the spy.
     * @param {String} name The name to build into a getter.
     * @param {String} type A single character representing the type of getter spies to create.
     * @param {Object[]} [spies] The list of spies to attach to the return object either as strings, configuration objects or a combination of both. See spyOnObject for more details.
     * @param {String} [prefix] The prefix to append to the spy name. Defaults to "get".
     * @returns {Function}
     */
    function createAccessorSpy(instance: any, name: string, type: string, spies?: any[], prefix?: string): jasmine.Spy {
        var config = ExtSpec.getResourceConfig(type),
            list = instance && instance[config.property],
            spy: jasmine.Spy,
            target;

        if (config.matcher(list, name)) {
            spy = createSpy(instance, name, config.suffix, prefix);

            if (spies) {
                target = {};
                processSpies(target, spies);
                spy.and.returnValue(target);
            }

            return spy;
        }

        throw new Error('The ' + config.property + ' property for this class does not contain the item "' + name + '".');
    }

    /**
     * Creates all spies on the given instance of the provided type.
     * @param {Object} instance The object on which to create the spies.
     * @param {String} type A single character representing the type of getter spies to create.
     * @param {String[]} list The list of items to spy, or undefined to create spies for all of the given type in the instance.
     */
    function createAccessorSpies(instance: any, type: string, list: string[]): void {
        var config: any,
            i: number,
            len: number,
            name: string;

        if (ExtSpec.isTypeOf(list, 'ar')) {
            for (i = 0, len = list.length; i < len; i += 1) {
                name = list[i];
                createAccessorSpy(instance, name, type);
            }
        } else {
            config = ExtSpec.getResourceConfig(type);
            list = instance && instance[config.property];

            if (ExtSpec.isTypeOf(list, 'ar')) {
                for (i = 0, len = list.length; i < len; i += 1) {
                    name = config.itemProperty ? list[i][config.itemProperty] : list[i];
                    createSpy(instance, name, config.suffix);
                }
            }
        }
    }

    /**
     * Creates getter spies for all models defined in the given instance.
     * @param {Object} instance The object on which to create the spies.
     * @param {String[]} [models] The list of models to spy, or undefined to create spies for all models in the instance.
     */
    export function createModelSpies(instance: any, models?: string[]): void {
        createAccessorSpies(instance, 'md', models);
    }

    /**
     * Creates a getter spy for the given model defined in the instance.
     * @param {Object} instance The object on which to create the spy.
     * @param {String} name The model name to build into a getter.
     * @param {Object[]} [spies] The list of spies to attach to the return object either as strings, configuration objects or a combination of both. See spyOnObject for more details.
     * @returns {Function}
     */
    export function createModelSpy(instance: any, name: string, spies?: any[]): jasmine.Spy {
        return createAccessorSpy(instance, name, 'md', spies);
    }

    /**
     * Creates getter spies for all views defined in the given instance.
     * @param {Object} instance The object on which to create the spies.
     * @param {String[]} [views] The list of views to spy, or undefined to create spies for all views in the instance.
     */
    export function createViewSpies(instance: any, views?: string[]): void {
        createAccessorSpies(instance, 'vw', views);
    }

    /**
     * Creates a getter spy for the given view defined in the instance.
     * @param {Object} instance The object on which to create the spy.
     * @param {String} name The view name to build into a getter.
     * @param {Object[]} [spies] The list of spies to attach to the return object either as strings, configuration objects or a combination of both. See spyOnObject for more details.
     * @returns {Function}
     */
    export function createViewSpy(instance: any, name: string, spies?: any[]): jasmine.Spy {
        return createAccessorSpy(instance, name, 'vw', spies);
    }

    /**
     * Creates getter spies for all controllers defined in the given instance.
     * @param {Object} instance The object on which to create the spies.
     * @param {String[]} [controllers] The list of controllers to spy, or undefined to create spies for all controllers in the instance.
     */
    export function createControllerSpies(instance: any, controllers?: string[]): void {
        createAccessorSpies(instance, 'ct', controllers);
    }

    /**
     * Creates a getter spy for the given controller defined in the instance.
     * @param {Object} instance The object on which to create the spy.
     * @param {String} name The controller name to build into a getter.
     * @param {Object[]} [spies] The list of spies to attach to the return object either as strings, configuration objects or a combination of both. See spyOnObject for more details.
     * @returns {Function}
     */
    export function createControllerSpy(instance: any, name: string, spies?: any[]): jasmine.Spy {
        return createAccessorSpy(instance, name, 'ct', spies);
    }

    /**
     * Creates getter spies for all stores defined in the given instance.
     * @param {Object} instance The object on which to create the spies.
     * @param {String[]} [stores] The list of stores to spy, or undefined to create spies for all stores in the instance.
     */
    export function createStoreSpies(instance: any, stores?: string[]): void {
        createAccessorSpies(instance, 'st', stores);
    }

    /**
     * Creates a getter spy for the given store defined in the instance.
     * @param {Object} instance The object on which to create the spy.
     * @param {String} name The store name to build into a getter.
     * @param {Object[]} [spies] The list of spies to attach to the return object either as strings, configuration objects or a combination of both. See spyOnObject for more details.
     * @returns {Function}
     */
    export function createStoreSpy(instance: any, name: string, spies?: any[]): jasmine.Spy {
        return createAccessorSpy(instance, name, 'st', spies);
    }

    /**
     * Creates getter spies for all refs defined in the given instance.
     * @param {Object} instance The object on which to create the spies.
     * @param {String[]} [refs] The list of refs to spy, or undefined to create spies for all refs in the instance.
     */
    export function createRefSpies(instance: any, refs?: string[]): void {
        createAccessorSpies(instance, 'rf', refs);
    }

    /**
     * Creates a getter spy for the given ref defined in the instance.
     * @param {Object} instance The object on which to create the spy.
     * @param {String} name The ref name to build into a getter.
     * @param {Object[]} [spies] The list of spies to attach to the return object either as strings, configuration objects or a combination of both. See spyOnObject for more details.
     * @returns {Function}
     */
    export function createRefSpy(instance: any, name: string, spies?: any[]): jasmine.Spy {
        return createAccessorSpy(instance, name, 'rf', spies);
    }

    /**
     * Creates accessor spies for all configs defined in the given instance.
     * @param {Object} instance The object on which to create the spies.
     * @param {String[]} [configs] The list of configs to spy, or undefined to create spies for all configs in the instance.
     */
    export function createConfigSpies(instance: any, configs?: string[]): void {
        var name: string,
            i: number,
            len: number;

        if (ExtSpec.isTypeOf(configs, 'ar')) {
            for (i = 0, len = configs.length; i < len; i += 1) {
                name = configs[i];
                createAccessorSpy(instance, name, 'cf', null, 'set');
                createAccessorSpy(instance, name, 'cf');
            }
        } else {
            for (name in instance.config) {
                if (instance.config.hasOwnProperty(name)) {
                    createSpy(instance, name, null, 'set');
                    createSpy(instance, name);
                }
            }
        }
    }

    /**
     * Creates accessor spies for the given config defined in the instance.
     * @param {Object} instance The object on which to create the spy.
     * @param {String} name The config name to build into a getter and setter.
     * @param {Object[]} [spies] The list of spies to attach to the getter return object either as strings, configuration objects or a combination of both. See spyOnObject for more details.
     * @returns {Function}
     */
    export function createConfigSpy(instance: any, name: string, spies?: any[]): jasmine.Spy {
        createSpy(instance, name, null, 'set');

        return createAccessorSpy(instance, name, 'cf', spies);
    }

    /**
     * Creates an object and attaches spies using the given configuration.
     * @param {Object[]} spies The list of spies to attach either as strings, configuration objects or a combination of both. See spyOnObject for more details.
     * @returns {Object}
     */
    export function createSpyObject(spies: any[]): any {
        var target = {};

        processSpies(target, spies);

        return target;
    }

    /**
     * Creates spies on an object that automatically return the root object for method chaining.
     * @param {String} baseName The name of spy the class.
     * @param {String[]} spies The array of method names to make spies.
     */
    export function createFluentSpyObject(baseName: string, spies: string[]): any {
        var i: number,
            len: number,
            fluent = jasmine.createSpyObj(baseName, spies);

        for (i = 0, len = spies.length; i < len; i += 1) {
            fluent[spies[i]].and.returnValue(fluent);
        }

        return fluent;
    }

    /**
     * Spies on methods within the target object, or creates them if they don't already exist.
     * @param {Object} target The target on which to assign the spies.
     * @param {Object[]} spies The list of spies to attach either as strings, configuration objects or a combination of both. Configurations must contain a name and an optional value and / or action in the form:
     * {
     *     name: 'methodName',
     *     value: ['subMethod1', 'subMethod2'],
     *     action: 'spy'
     * }
     * 
     * Supported actions are:
     *     call: equivalent of and.callThrough(),
     *     fake: equivalent of and.callFake(),
     *     spy: equivalent of and.returnValue(jasmine.createSpyObj()),
     *     return: equivalent of and.returnValue(),
     *     object: equivalent of target[name] = jasmine.createSpyObj(),
     *     property: equivalent of target[name] = value,
     *     fluent: equivalent of and.returnValue(target),
     *     none: do nothing
     * 
     * If unspecified, the return action is assumed unless the value is a:
     *     function: assume fake,
     *     array: assume spy,
     *     undefined: assume none
     */
    export function spyOnObject(target: any, spies: any[]): void {
        processSpies(target, spies);
    }

    /**
     * Spies on methods within the given namespace, or creates them if they don't already exist.
     * @param {String} namespace The namespace (using dot notation) on which to assign the spies.
     * @param {Object[]} spies The list of spies to attach either as strings, configuration objects or a combination of both. See spyOnObject for more details.
     * @returns {Object}
     */
    export function spyOnNamespace(namespace: string, spies: any[]): any {
        var target = ExtSpec.ensureNamespace(namespace);

        processSpies(target, spies);

        return target;
    }

    /**
     * Fetches the return value assigned to a spy as part of a chain.
     * @param {Object} instance The object instance that contains spies.
     * @param {String} chain The method chain using dot notation but without parens.
     * @returns {Object}
     */
    export function getSpyReturn(instance: any, chain: string): any {
        var names: string[] = chain.split('.'),
            value: any = instance,
            name: string,
            len: number,
            i: number;

        for (i = 0, len = names.length; i < len; i += 1) {
            name = names[i];

            if (!value.hasOwnProperty(name)) {
                throw new Error(name + ' could not be found on the instance.');
            }

            value = value[name];

            if (jasmine.isSpy(value)) {
                value = value.and.exec();
            }
        }

        return value;
    }
}